package com.atguigu.tingshu.user.service.impl;
import java.util.Date;


import com.atguigu.tingshu.album.client.AlbumInfoFeignClient;
import com.atguigu.tingshu.common.constant.PublicConstant;
import com.atguigu.tingshu.common.constant.RedisConstant;


import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.api.WxMaUserService;
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;

import com.alibaba.fastjson.JSONObject;
import com.atguigu.tingshu.common.execption.GuiguException;

import com.atguigu.tingshu.common.rabbit.constant.MqConst;
import com.atguigu.tingshu.common.rabbit.service.RabbitService;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.common.util.AuthContextHolder;
import com.atguigu.tingshu.common.util.MongoUtil;
import com.atguigu.tingshu.model.user.*;
import com.atguigu.tingshu.user.mapper.UserInfoMapper;
import com.atguigu.tingshu.user.mapper.UserPaidAlbumMapper;
import com.atguigu.tingshu.user.mapper.UserPaidTrackMapper;
import com.atguigu.tingshu.user.service.UserInfoService;
import com.atguigu.tingshu.vo.album.TrackListVo;
import com.atguigu.tingshu.vo.album.TrackStatMqVo;
import com.atguigu.tingshu.vo.album.TrackStatVo;
import com.atguigu.tingshu.vo.user.UserCollectVo;
import com.atguigu.tingshu.vo.user.UserInfoVo;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import org.bson.types.ObjectId;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.RsaVerifier;
import org.springframework.security.jwt.crypto.sign.Signer;
import org.springframework.stereotype.Service;
import org.apache.commons.lang3.StringUtils;

import org.springframework.util.CollectionUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.*;

import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static com.alibaba.fastjson.JSON.toJSONString;

@Slf4j
@Service
@SuppressWarnings({"unchecked", "rawtypes"})
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> implements UserInfoService {


    @Autowired
    private WxMaService wxMaService;

    @Autowired
    private Signer rsaSigner;

    @Autowired
    private UserInfoMapper userInfoMapper;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private RabbitService rabbitService;

    @Autowired
    private UserPaidAlbumMapper userPaidAlbumMapper;

    @Autowired
    private UserPaidTrackMapper userPaidTrackMapper;

    @Autowired
    private MongoTemplate mongoTemplate;

    @Autowired
    private AlbumInfoFeignClient albumInfoFeignClient;

    @Override
    public Map<String, Object> wxLogin(String code) {
        WxMaJscode2SessionResult sessionInfo  =null;
        String accessTokenKey = null;
        //1.校验code是否存在
        if (StringUtils.isEmpty(code)){
            //为空,code不存在
            throw new GuiguException(201,"code码不存在,登录失败");
        }
        //2.得到一个发起微信调用的对象
        //2.1得到一个微信用户service对象()
        WxMaUserService userService = wxMaService.getUserService();
        //2.2得到一个SessionInfo对象
        try {
             sessionInfo = userService.getSessionInfo(code);
        } catch (WxErrorException e) {
            log.error("请求微信小程序登录内部出现异常,{}",e.getMessage());
            throw new GuiguException(201,"code码不存在,登录失败");
        }
        //2.3得到openId(用户的唯一标识)
        String openId = sessionInfo.getOpenid();
        if (StringUtils.isEmpty(openId)){
            throw new GuiguException(201,"openId码不存在,登录失败");
        }
        //2.4查询Redis中是否有token
        accessTokenKey = RedisConstant.USER_LOGIN_KEY_PREFIX+openId;
        String tokenFromRedis = redisTemplate.opsForValue().get(accessTokenKey);
        if (!StringUtils.isEmpty(tokenFromRedis)){
            HashMap<String, Object> map = new HashMap<>();
            map.put("token",tokenFromRedis);
            return map;
        }

        //3.根据openId查询用户信息
        UserInfo userInfo = userInfoMapper.selectOne(new LambdaQueryWrapper<UserInfo>().eq(UserInfo::getWxOpenId, openId));
        if (userInfo==null){
            //为空,注册一个新用户
            userInfo = new UserInfo();
            userInfo.setWxOpenId(openId);
            userInfo.setNickname("ts-["+System.currentTimeMillis()+"]");
            userInfo.setAvatarUrl("https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg");
            userInfo.setIsVip(0); //初始用户不是vip,默认值为0
            userInfo.setCreateTime(new Date());
            int insert = userInfoMapper.insert(userInfo);
            log.info("初始化用户状态{}",insert>0?"success":"fail");

            //初始化这个新用户的账号
            /**
             * param1:发送的交换机
             * param2:路由键
             * param3:发送的消息内容(user_id)
             * 网络传输使用字符串是最稳定,最不容易丢失数据的
             * 序列化的作用一般有2个:
             * 1.网络传输用
             * 2.文件持久化用
             */
            rabbitService.sendMessage(MqConst.EXCHANGE_ACCOUNT,MqConst.ROUTING_USER_REGISTER,userInfo.getId().toString());
            log.info("上游用户微服务发送消息:{}给下游账户微服务成功",userInfo.getId());

        }
        //4.自定义登录状态
        HashMap<String, Object> map = new HashMap<>();
        //传统方案使用UUID没有代表性
       // String token= UUID.randomUUID().toString().replace("-"," ");
        //用JWT生成一个有代表性的jsonwebToken
        //4.0构建载荷部分的自定义信息
        String jsonWebToken = getJsonwebToken(openId, userInfo.getId());

        //存到Redis中
        //给Redis中的key设置过期时间
        // 存到session中（其它为服务session对象：作用域下）
        // 存到mysql中 存到redis中也可以。
        // 设置一个过期时间（redis的key设置过期时间）
        // 这个时间到底控制在多少：时间设置的比较长：优点：1）用户不需要频繁登录 缺点： 2）无法避免僵尸用户占用redis内存  3）相对来说不安全
        //                    时间设置的比较短：优点：安全性高 内存占用不是很多 缺点：用户体验非常差 需要频繁登录
        // 双令牌机制就可以解决 1）用户体验好   2）可以避免僵尸用户占用redis的内存  3）还比较安全
        String refreshTokenKey=RedisConstant.USER_LOGIN_KEY_PREFIX+"refresh:"+openId;
        //redisTemplate.opsForValue().set(accessTokenKey,jsonWebToken,30, TimeUnit.MINUTES );
        //测试用
        redisTemplate.opsForValue().set(accessTokenKey,jsonWebToken,25, TimeUnit.DAYS );
        redisTemplate.opsForValue().set(refreshTokenKey,jsonWebToken,30,TimeUnit.DAYS);
        map.put("token",jsonWebToken);//过期之后生成


        return map;
    }

    private String getJsonwebToken(String openId, Long userId) {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("userId", userId);
        jsonObject.put("openId", openId);
        //4.1生成一个JWT对象
        Jwt jwt = JwtHelper.encode(toJSONString(jsonObject),rsaSigner);
        //4.2得到jsonwebTkoen
        String jsonWebToken = jwt.getEncoded();
        return jsonWebToken;
    }

    @Override
    public Map<String, Object> refreshToken() {
        //生成一个Access_Token和Refresh_Token
        //把这两个令牌存入Redis中
        //返回给前端

        //1.从请求头中获取到Access_Token
        //1.1获取请求属性对象
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        //1.2根据请求属性获取请求对象
        HttpServletRequest request = requestAttributes.getRequest();
        //1.3根据请求对象获取请求头
        String jsonWebToken = request.getHeader("token");
        //1.4根据jsonWebToken解析自定义数据
        Jwt jwt = JwtHelper.decodeAndVerify(jsonWebToken, new RsaVerifier(PublicConstant.PUBLIC_KEY));
        //1.5得到Jwt的载荷
        String claims = jwt.getClaims();
        //1.6将claims反序列化成一个Json格式的对象
        Map<String,Object> map = JSONObject.parseObject(claims, Map.class);
        //从map中获取到自定义中载荷的数据
        String userId = (String) map.get("userId");
        Long openId = (Long) map.get("openId");
        String newjsonwebToken = getJsonwebToken(userId, openId);
        String tokenKey=RedisConstant.USER_LOGIN_KEY_PREFIX+openId;
        String refreshTokenKey=RedisConstant.USER_LOGIN_KEY_PREFIX+"refresh:"+openId;
        String fromRedisRefreshToken = redisTemplate.opsForValue().get(refreshTokenKey);

        //判断refresh_Token是否过期
        if (!StringUtils.isEmpty(fromRedisRefreshToken)){
            //没过期,可以换新令牌
            redisTemplate.opsForValue().set(tokenKey,newjsonwebToken,30, TimeUnit.MINUTES );
            redisTemplate.opsForValue().set(refreshTokenKey,newjsonwebToken,30,TimeUnit.DAYS);

            HashMap<String, Object> map1 = new HashMap<>();
            map1.put("token",newjsonwebToken);
            map1.put("refreshToken",fromRedisRefreshToken);
            return map1;
        }else{
            throw new GuiguException(201,"refreshToken已过期");
        }

    }

    //获取用户信息
    @Override
    public UserInfoVo getUserInfoVo(Long userId) {

        //1.前端发送的这个获取用户登录信息的请求 是否携带了登录信息(user,openId)
        //2.如果没带(去登录)
        //3.如果带了要校验(是否携带对了:和redis比对)
        //4.如果校验通过 才能返回登录后的用户信息
        //5.如果校验役通过 去登录。

        UserInfo userInfo = userInfoMapper.selectById(userId);

        if (userInfo==null){
            throw new GuiguException(201,"用户信息未登录");
        }
        UserInfoVo userInfoVo = new UserInfoVo();
        BeanUtils.copyProperties(userInfo,userInfoVo); //这两个类中字段名字一定要一样 且 这两类的字段类型也一定要一样

        return userInfoVo;
    }

    //更新用户信息
    @Override
    public void updateUser(UserInfoVo userInfoVo) {
        Long userId = AuthContextHolder.getUserId();
        UserInfo userInfo = userInfoMapper.selectById(userId);
        if (userInfo==null){
            throw new GuiguException(201,"用户信息未登录");
        }
        userInfo.setNickname(userInfoVo.getNickname());
        userInfo.setAvatarUrl(userInfoVo.getAvatarUrl());
        userInfoMapper.updateById(userInfo);
    }

    /**
     * 获取用户已购专辑的已购歌曲
     * @param albumId
     * @param userId
     * @return
     */
    @Override
    public Map<Long, String> getUserPaidAlbumTrackList(Long albumId, Long userId) {
        //1.查询用户购买的专辑下的声音列表
        List<UserPaidTrack> userPaidTracks = userPaidTrackMapper.selectList(new LambdaQueryWrapper<UserPaidTrack>()
                .eq(UserPaidTrack::getUserId, userId)
                .eq(UserPaidTrack::getAlbumId, albumId));
        //2.将集合转换为Map key:声音id,value:一个字符串 (做买过的标识)
        Map<Long, String> collect = userPaidTracks.stream().collect(Collectors.toMap(UserPaidTrack::getTrackId, v -> "1"));

        return collect;
    }


    /**
     * 判断用户是否购买了专辑
     */
    @Override
    public Boolean getUserPaidAlbum(Long albumId, Long userId) {
        //1.查询用户购买的专辑列表
        //1.1 因为albumId是主键不会重复,所以查出来的只有一个,或者查不出来
        UserPaidAlbum userPaidAlbum = userPaidAlbumMapper.selectOne(new LambdaQueryWrapper<UserPaidAlbum>()
                .eq(UserPaidAlbum::getUserId, userId)
                .eq(UserPaidAlbum::getAlbumId, albumId));
        return userPaidAlbum!=null;
    }

    @Override
    public Boolean collect(Long trackId) {
        Long userId = AuthContextHolder.getUserId();
        //0.先查询有没有记录
        //0.1创建一个条件对象
        Criteria criteria = Criteria.where("userId").is(userId).and("trackId").is(trackId);
        //0.2创建一个查询对象
        Query query = new Query(criteria);
        String collectionName = MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_COLLECT, userId);
        long count = mongoTemplate.count(query, UserCollect.class, collectionName);
        if (count==0){
            //保存收藏对象到mong中
            UserCollect userCollect = new UserCollect();
            userCollect.setId(ObjectId.get().toString());
            userCollect.setUserId(userId);
            userCollect.setTrackId(trackId);
            userCollect.setCreateTime(new Date());
            mongoTemplate.save(userCollect,collectionName);

            TrackStatMqVo  trackStatMqVo= prepareTrackMgDto(trackId,1);
            rabbitService.sendMessage(MqConst.EXCHANGE_TRACK,MqConst.ROUTING_TRACK_STAT_UPDATE,toJSONString(trackStatMqVo));
            return true;
        }else {
            mongoTemplate.remove(query,UserCollect.class,collectionName);
            TrackStatMqVo  trackStatMqVo= prepareTrackMgDto(trackId,-1);
            rabbitService.sendMessage(MqConst.EXCHANGE_TRACK,MqConst.ROUTING_TRACK_STAT_UPDATE,toJSONString(trackStatMqVo));
            //取消收藏
            return false;
        }
    }

    @Override
    public Boolean isCollect(Long trackId) {
        Long userId = AuthContextHolder.getUserId();
        //0.先查询有没有记录
        //0.1创建一个条件对象
        Criteria criteria = Criteria.where("userId").is(userId).and("trackId").is(trackId);
        //0.2创建一个查询对象
        Query query = new Query(criteria);
        String collectionName = MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_COLLECT, userId);
        long count = mongoTemplate.count(query, UserCollect.class, collectionName);

        return count>0;
    }


    /**
     * 分页查询用户收藏列表
     * @param userCollectVoPage
     * @return
     */
    @Override
    public IPage<UserCollectVo> findUserCollectPage( IPage<UserCollectVo> userCollectVoPage) {
        Long userId = AuthContextHolder.getUserId();
        Query query = Query.query(Criteria.where("userId").is(userId));
        Long pn = userCollectVoPage.getCurrent();
        Long pz = userCollectVoPage.getSize();
        int page = (int) ((pn-1)*pz);
        PageRequest pageRequest = PageRequest.of(page, pz.intValue(), Sort.Direction.DESC, "updateTime");
        query.with(pageRequest);

        String collectionName = MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_COLLECT, userId);
        //查询总记录数
        long totalCount = mongoTemplate.count(query.limit(-1), UserCollect.class, collectionName);
        //查询结果
        List<UserCollect> userCollects = mongoTemplate.find(query, UserCollect.class, collectionName);
        //收集声音的id
        List<Long> trackIds = userCollects.stream().map(UserCollect::getTrackId).collect(Collectors.toList());

        //远程查询数据库(根据声音id查询声音的信息赋值)
        Result<List<TrackListVo>> trackListVo =albumInfoFeignClient.getTrackListVo(trackIds);
        List<TrackListVo> trackListVoData = trackListVo.getData();
        if (CollectionUtils.isEmpty(trackListVoData)){
            throw new GuiguException(201,"声音集合不存咋");
        }

        //将trackListVoData转为map(方便取数据)
        Map<Long, TrackListVo> trackListVoMap = trackListVoData.stream().collect(Collectors.toMap(TrackListVo::getTrackId, v -> v));

        List<UserCollectVo> userCollectVoList = userCollects.stream().map(userCollect -> {
            UserCollectVo userCollectVo = new UserCollectVo();
            userCollectVo.setTrackId(userCollect.getTrackId());
            userCollectVo.setCreateTime(userCollect.getCreateTime());
            userCollectVo.setAlbumId(trackListVoMap.get(userCollect.getTrackId()).getAlbumId());
            userCollectVo.setTrackTitle(trackListVoMap.get(userCollect.getTrackId()).getTrackTitle());
            userCollectVo.setCoverUrl(trackListVoMap.get(userCollect.getTrackId()).getCoverUrl());
            return userCollectVo;
        }).collect(Collectors.toList());
        userCollectVoPage.setTotal(totalCount);
        userCollectVoPage.setRecords(userCollectVoList);


        return userCollectVoPage;
    }

    @Override
    public Boolean isSubscribe(Long albumId) {
        Long userId = AuthContextHolder.getUserId();
        Query query = Query.query(Criteria.where("userId").is(userId).and("albumId").is(albumId));
        long count = mongoTemplate.count(query, MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_SUBSCRIBE, userId));
        return count > 0;
    }


    private TrackStatMqVo prepareTrackMgDto(Long trackId,Integer count) {
        TrackStatMqVo trackStatMqVo = new TrackStatMqVo();
        trackStatMqVo.setBusinessNo(UUID.randomUUID().toString().replace("-"," "));
        trackStatMqVo.setTrackId(trackId);
        trackStatMqVo.setStatType("0702");
        trackStatMqVo.setCount(count);

        return trackStatMqVo;
    }
}
